Skip to content

4.0.0: twelve new packages, optimistic concurrency, and a dashboard that shows them - #46

Merged
ivanvyd merged 12 commits into
mainfrom
feature/v4-dashboard-and-version
Jul 30, 2026
Merged

4.0.0: twelve new packages, optimistic concurrency, and a dashboard that shows them#46
ivanvyd merged 12 commits into
mainfrom
feature/v4-dashboard-and-version

Conversation

@ivanvyd

@ivanvyd ivanvyd commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Cuts 4.0.0 and finishes the dashboard work behind it.

What this adds

  • The dashboard surfaces every feature package you install. Uptime (24H and longest outage), a paged alert history, a leadership badge, an AI explain button, and a metrics view — each appearing because a service resolves, not because a flag was set. An application that installs none of them gets the board unchanged.
  • A collapsible side menu — overview, a section per group with its tally and worst state, and the alerts, metrics, event-log and about views. A rail rather than an overlay drawer, because this package ships no JavaScript and a drawer needs a focus trap to be honest about keyboard use.
  • The board opens sectioned by group. A group is a partition, so the sectioned view answers "what is wrong, and where" without the reader doing it in their head.
  • Alert history is persisted and paged through the application's own IStateProvider, so it survives a redeploy on any durable provider. No second storage contract, and no provider had to learn about alerts.
  • Alerting is configurable from the board — the four settings the dispatcher reads live, plus a test alert through the real sinks. Its queue capacity and history length are fixed at construction, so they are shown as facts rather than offered as controls that would quietly do nothing.
  • AddHealthieMetrics() reads the Healthie.NET meter in-process, alongside any OpenTelemetry exporter rather than instead of one.

Bugs this fixes

  • The board ignored PulseSchedule entirely. It read state.Interval for the rate column, the aggregate checks-per-minute and the interval picker — and that field is documented as ignored once Schedule is set. A cron checker advertised a rate it was not running at and offered a picker that stored a field nothing reads. SetIntervalAsync now clears the schedule, so choosing an interval takes effect.
  • The alerts panel could never fill without a sink. The dispatcher skipped subscribing when none was registered — right while sinks were the only consumer, wrong once the dashboard read the same history.
  • AlertHistory threw on the first alert at capacity zero, and HistoryLength had no validation.
  • The initially-selected checker never had its uptime read; a doc block had two summaries; the replica badge was documented as follower-only when it renders both.

Compatibility

Source-compatible — see docs/migration-v3-to-v4.md. One binary break (HealthieTools' constructor gained a defaulted parameter), and the relational providers add a version column on startup.

Verification

  • Release build: 0 errors, 0 warnings
  • Unit: 529/529 on net8.0 and net10.0, including real Redis, PostgreSQL, SQL Server and CosmosDB containers
  • E2E: 53/53 in Chromium, across timer and Quartz schedulers
  • Read-only mode driven live: every read survives, every mutating control is gone

ivanvyd added 12 commits July 30, 2026 10:39
The dispatcher skipped subscribing entirely when no IAlertSink was
present. That was right while sinks were the only consumer of an alert;
it is not right now that the dashboard reads the same history, because
an application that installs alerting for the panel and configures
delivery later gets a panel that can never fill.

Also loads uptime for the checker the board selects on load. That
selection is made by MarkLoaded rather than by a click, so it never
went through the path that reads it, and the panel opened missing the
columns every later selection showed.
Turns the Unreleased section into 4.0.0, reordered to Keep a Changelog's
Added/Fixed/Security and with the two Fixed blocks merged. The intro says
plainly that the major number is about the size of the release rather
than a break in it, because every entry under it is additive.

Documents the panels the board grows per feature package, in both the
repository README and the dashboard package's own, including which of
them survive read-only mode and why EXPLAIN does not.
A zero-capacity alert history threw on the first alert rather than
discarding it: trimming ran before the enqueue, so an empty queue was
the case on every call. Trimming after the enqueue removes the special
case instead of guarding it, and HealthieAlertOptions.HistoryLength now
clamps to a minimum of one the way MaxHistoryLength does.

The rest is tidying the review found: the six insight contracts split
one per file, since IHealthieInsights.cs was the only file in
Abstractions holding more than one public type and no type of that name
existed; a doc block that ended up with two summaries; an unused
HasInsights property; the alerts markup indented a level deeper than its
siblings; a CSS variable fallback no other call site carries and no
theme leaves undefined; and _history grouped with the other injected
fields rather than after an unrelated counter.
Twelve packages are new since v3.1.4, not eleven -- counted against the
tag rather than from the list in the section, which had drifted.
The intro claimed nothing public changed shape. Diffing the public API
of the eight pre-3.1.4 packages against the tag says otherwise:
HealthieTools' constructor gained a defaulted HealthieMcpOptions
parameter when it was fixed to read MaxHistoryPageSize. Source-
compatible, binary-breaking, and worth naming -- it is the reason the
major number is honest rather than only a size claim.
The sample installs the uptime package, so the panel grew a 24H cell and
the hard-coded count of three failed. A count was the wrong assertion
anyway: 24H appears once a segment exists and WORST only after an
outage, both of which depend on how long the app has been up. Asserts
the three that are always there instead, and that nothing unexpected
joins them -- scoped to the stats grid, since the same label class
dresses the GROUP and TAGS editors below it.
Both docs said it appears only on a follower. It appears whenever the
leader-election package is installed, reading LEADER or FOLLOWER and
naming the replica on hover -- which is the more useful behaviour when
the package is opt-in and installed precisely because there is more than
one replica. Caught by running a leader-elected instance rather than by
reading the markup.
Grouping is on when the board opens. A group is a partition, so the
sectioned view answers what is wrong and where without the reader doing
it in their head; checkers with no group collect under one heading
rather than vanishing. The stale tag-era names went with it --
_groupByTags and TagGroup partitioned on Group, which is the exact
confusion CLAUDE.md warns about, preserved in the identifiers.

The board ignored PulseSchedule entirely. It read state.Interval for the
rate column, the aggregate CHECKS/MIN and the interval picker, and
Interval is documented as ignored once Schedule is set -- so a cron
checker advertised a rate it was not running at, was summed into the
aggregate at that rate, and offered a picker that stored a field nothing
reads. It now reads EffectiveSchedule, shows a cron expression as one,
counts cron checkers separately rather than guessing a rate for them,
and disables the picker while an expression is in force.

A cron expression can also be set from the board. The scheduler judges
it before anything is stored, through a new defaulted
IPulseScheduler.TryValidateSchedule, because Cronos, Quartz and Temporal
do not agree on dialects and the only answer worth having is from the
implementation that will run it. SetIntervalAsync now clears the
schedule, so choosing an interval takes effect.

The side menu is a rail, not a drawer: this package ships no JavaScript,
and an overlay drawer needs a focus trap to be honest about keyboard
use. Collapsed it keeps its icons and its toggle; below 820px it becomes
a scrolling strip above the list.
… the old default

Three E2E grouping tests clicked the GROUP button to turn grouping on.
With it on by default that click turns it off, so they asserted grouped
structure on an ungrouped board -- one failed and two passed by luck,
which is worse. The one that needed a flat count for comparison now
takes the grouped count first and toggles to flat, which is the
comparison it always wanted; the other two simply stop toggling.

New coverage: the board opens grouped, the side menu narrows to a group
and back, the cron editor applies a valid expression and refuses a bad
one without disturbing what is stored, and the interval picker is inert
for a cron checker and live for an interval one.

The cron E2E tests first read the box before the click had round-tripped
-- the box is already visible for whichever checker was selected on load,
so waiting on it proves nothing. They wait on the panel title instead.

Also: the unit test double derived from PulseChecker, so assembly
scanning discovered it and could not construct it, which failed twenty
registration, MCP and AI tests that have nothing to do with schedules. It
takes only a state provider now, like every other checker in that
assembly. Caught by running the whole suite rather than the filter.
…metrics

The alerts list was a full-bleed band that shoved the whole board down
when it opened, edge to edge with none of the card treatment every other
panel has and a CLOSE button spanning half the width. It is now a view in
the middle column, switched from the side menu alongside a new metrics
view, so opening it changes one column instead of displacing the page.

The history is written through the application's own IStateProvider, so a
deployment on CosmosDB, Postgres or Redis keeps its alerts across a
redeploy and one on the in-memory provider does not. No second storage
contract, and no provider had to learn about alerts. Reads are paged --
an operator arriving after a restart is looking for what happened before
it, which is exactly what a last-twenty list throws away -- and the view
says how many are kept, of what cap, in which provider.

Sinks are listed whether or not they have done anything, with delivered
and failed counts and the last error. "Nothing has alerted" and "nothing
is configured to deliver" looked identical and mean opposite things. A
sink that recovers stops being shown as failing, because what matters is
whether it is working now.

Alerting is configurable from the board: minimum severity, deduplication
window, delivery timeout and recovery alerts, which are the four the
dispatcher reads on every alert rather than snapshotting. Its queue
capacity and history length are fixed when it is built, so they are shown
as facts rather than offered as controls that would quietly do nothing.
A test alert goes through the real sinks, because the only way to find
out a webhook URL is wrong is to use it.

Metrics come from a MeterListener on the Healthie.NET meter, opt-in
through AddHealthieMetrics: it reads the instruments this library already
emits without competing with an OpenTelemetry exporter on the same meter.
Overlapped triggers get their own card -- a checker whose check outlasts
its interval looks healthy and is quietly running at a fraction of the
rate it was asked to, and that is the only place it shows.

Three E2E tests interacted with the detail panel straight after clicking
a row. The panel's controls are already on screen for whatever was
selected on load, so waiting on them proved nothing: the tag test was
editing the wrong checker and failing elsewhere. They wait on the panel
title now, through one helper.
Removing the dead band rules deleted from ".hpm-alerts {" to the cron
marker, and the whole side-menu block sat between the two -- so the rail
lost every rule it had and rendered as wrapped unstyled chips. Put back
where it was, in front of the cron rules, so the 820px override at the
end of the file still wins over it.
Removes an AlertPage.Empty factory nothing called, and the _uptimeFor
cache: it short-circuited a re-read of the same checker, so a board left
open on one checker showed a 24H figure that never moved again. Dropping
it deletes a field and a branch and makes clicking the selected row the
way to refresh.

Documents the alert history's persistence and paging, the sink panel, the
settings the board can change and the ones it deliberately cannot, and
the metrics view -- in the changelog and the dashboard README -- plus a
v3-to-v4 migration guide covering the one binary break and the ALTER the
relational providers run on startup.
@ivanvyd
ivanvyd merged commit 443ca38 into main Jul 30, 2026
7 of 8 checks passed
@ivanvyd
ivanvyd deleted the feature/v4-dashboard-and-version branch July 30, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant